/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.IO;
using System.Globalization;
using System.Configuration.Assemblies;

namespace DaveSexton.DocProject
{
  /// <remarks>
  /// This class is thread-safe.
  /// </remarks>
  [Serializable]
  public abstract class AssemblyLoader
  {
    #region Public Properties
    public AssemblyName LoadAssemblyName
    {
      get
      {
        lock (sync)
        {
          if (loadAssembly == null)
            loadAssembly = CreateAssemblyName();

          return loadAssembly;
        }
      }
    }

    public Assembly Assembly
    {
      get
      {
				lock (sync)
				{
					return assembly;
				}
      }
      protected set
      {
				lock (sync)
				{
					assembly = value;
					exception = null;

					if (value == null)
					{
						assemblyLoaded = false;
						triedLoadAssembly = false;
					}
					else
					{
						assemblyLoaded = true;
						triedLoadAssembly = true;
					}
				}
      }
    }

		public Exception LoadException
		{
			get
			{
				lock (sync)
				{
					return exception;
				}
			}
		}

    public bool CompareVersions
    {
      get
      {
        return compareVersions;
      }
      protected set
      {
        compareVersions = value;
      }
    }

    public bool CompareCulture
    {
      get
      {
        return compareCulture;
      }
			protected set
      {
        compareCulture = value;
      }
    }
    #endregion

    #region Private / Protected
    private AssemblyName loadAssembly;
    // NOTE: The assembly field cannot be serialized otherwise instances of this class cannot be passed to 
    // cross-AppDomain invocations because the target AppDomain will attempt to unwrap the assembly and will
    // fail.  As is, an instance of this class can be passed and then in the target AppDomain TryLoadAssembly
    // can be called again (which is why NonSerialized is important on the triedLoadAssembly and assemblyLoaded
    // fields below as well)
    [NonSerialized]
    private Assembly assembly;
    [NonSerialized]
    private volatile bool triedLoadAssembly, assemblyLoaded;
		private Exception exception;
    private volatile bool compareVersions, compareCulture;
    private readonly object sync = new object();
    #endregion

    #region Constructors
    /// <summary>
    /// Constructs a new instance of the <see cref="AssemblyLoader" /> class for derived classes.
    /// </summary>
    protected AssemblyLoader()
    {
    }

		/// <summary>
		/// Constructs a new instance of the <see cref="AssemblyLoader" /> class for derived classes.
		/// </summary>
		protected AssemblyLoader(bool compareVersions, bool compareCulture)
		{
			this.compareVersions = compareVersions;
			this.compareCulture = compareCulture;
		}
    #endregion

    #region Methods
    private static bool CompareKeys(byte[] key1, byte[] key2)
    {
      if (key1 == null || key2 == null || key1.Length != key2.Length)
        return false;

      for (int b = 0; b < key1.Length; b++)
        if (key1[b] != key2[b])
          return false;

      return true;
    }

    protected abstract AssemblyName CreateAssemblyName();

		public bool TryLoadAssembly()
		{
			return TryLoadAssembly(true);
		}

    [System.Diagnostics.DebuggerHidden]
		private bool TryLoadAssembly(bool attachToAppDomain)
    {
      if (triedLoadAssembly)
        return assemblyLoaded;

      lock (sync)
      {
        if (triedLoadAssembly)
          return assemblyLoaded;

        triedLoadAssembly = true;

				if (attachToAppDomain)
					AttachToCurrentAppDomain(false);

        try
        {
          // NOTE: allow LoadAssemblyName to be null for COM types
          return assemblyLoaded = LoadAssembly(LoadAssemblyName);
        }
				catch (ArgumentNullException ex) { exception = ex; }
				catch (FileNotFoundException ex) { exception = ex; }
				catch (FileLoadException ex) { exception = ex; }
				catch (BadImageFormatException ex) { exception = ex; }

        return false;
      }
    }

    /// <remarks>
    /// This method is required in case the framework attempts to resolve an assembly that has already been loaded.
    /// In that case, the same assembly is returned.  It is unknown why this occurs, but it has in testing.
    /// </remarks>
		protected void AttachToCurrentAppDomain(bool allowLoadOnDemand)
    {
      AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs e)
      {
				// In testing a file path could be returned by e.Name instead of an assembly name, at least when running in Visual Studio
				if (!System.IO.Path.IsPathRooted(e.Name))
				{
					lock (sync)
					{
						if (assemblyLoaded || allowLoadOnDemand)
						{
							AssemblyName loadName = LoadAssemblyName;

							if (loadName != null)
							{
								AssemblyName targetName = new AssemblyName(e.Name);

								if (string.Equals(loadName.Name, targetName.Name, StringComparison.OrdinalIgnoreCase)
									&& CompareKeys(loadName.GetPublicKeyToken(), targetName.GetPublicKeyToken())
									&& (!compareVersions || loadName.Version.Equals(targetName.Version))
									&& (!compareCulture || loadName.CultureInfo.Equals(targetName.CultureInfo)))
								{
									if (assembly != null || (allowLoadOnDemand && TryLoadAssembly(false)))
										return assembly;
								}
							}
						}
					}
				}

        return null;
      };
    }

    protected virtual bool LoadAssembly(AssemblyName name)
    {
			lock (sync)
			{
				assembly = Assembly.Load(name);
				return assembly != null;
			}
    }
    #endregion
  }
}
